]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
appDisplay: Animate AllView and FrequentView
authorCarlos Soriano <carlos.soriano89@gmail.com>
Tue, 17 Jun 2014 17:10:54 +0000 (19:10 +0200)
committerCarlos Soriano <carlos.soriano89@gmail.com>
Mon, 1 Sep 2014 20:34:51 +0000 (22:34 +0200)
Following design decision, we want to animate AllView and FrequentView
when opening and closing with a swarm spring form.

This involves a few changes needed to allow that, since from some time
now, we are animating page changes in viewSelector, using only a fade
transition. However now we want to let appDisplay and iconGrid apply its
own animation.

For that we special case the change to and from apps page on
viewSelector to let appDisplay to animate its own items, using and API
on appDisplay which at the same time uses an API on iconGrid.

Thanks Florian Müllner for the debugging work

https://bugzilla.gnome.org/show_bug.cgi?id=734726

js/ui/appDisplay.js
js/ui/iconGrid.js
js/ui/overview.js
js/ui/viewSelector.js

index 8b00cfeb701c341e290978c20895cd5d1ea015c5..c86f5e502d68fe1dd5b7df5d85a7d13e9ccd25c4 100644 (file)
@@ -34,7 +34,9 @@ const MIN_COLUMNS = 4;
 const MIN_ROWS = 4;
 
 const INACTIVE_GRID_OPACITY = 77;
-const INACTIVE_GRID_OPACITY_ANIMATION_TIME = 0.40;
+// This time needs to be less than IconGrid.EXTRA_SPACE_ANIMATION_TIME
+// to not clash with other animations
+const INACTIVE_GRID_OPACITY_ANIMATION_TIME = 0.24;
 const FOLDER_SUBICON_FRACTION = .4;
 
 const MIN_FREQUENT_APPS_COUNT = 3;
@@ -177,6 +179,38 @@ const BaseAppView = new Lang.Class({
                 this.selectApp(id);
             }));
         }
+    },
+
+    _doSpringAnimation: function(animationDirection) {
+        this._grid.actor.opacity = 255;
+        this._grid.animateSpring(animationDirection,
+                                 Main.overview.getShowAppsButton());
+    },
+
+    animate: function(animationDirection, onComplete) {
+        if (animationDirection == IconGrid.AnimationDirection.IN) {
+            let toAnimate = this._grid.actor.connect('notify::allocation', Lang.bind(this,
+                function() {
+                    this._grid.actor.disconnect(toAnimate);
+                    // We need to hide the grid temporary to not flash it
+                    // for a frame
+                    this._grid.actor.opacity = 0;
+                    Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
+                                   Lang.bind(this, function() {
+                                       this._doSpringAnimation(animationDirection)
+                                  }));
+                }));
+        } else {
+            this._doSpringAnimation(animationDirection);
+        }
+
+        if (onComplete) {
+            let animationDoneId = this._grid.connect('animation-done', Lang.bind(this,
+                function () {
+                    this._grid.disconnect(animationDoneId);
+                    onComplete();
+                }));
+        }
     }
 });
 Signals.addSignalMethods(BaseAppView.prototype);
@@ -323,7 +357,7 @@ const AllView = new Lang.Class({
         this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout() });
         let box = new St.BoxLayout({ vertical: true });
 
-        this._currentPage = 0;
+        this._grid.currentPage = 0;
         this._stack.add_actor(this._grid.actor);
         this._eventBlocker = new St.Widget({ x_expand: true, y_expand: true });
         this._stack.add_actor(this._eventBlocker);
@@ -455,14 +489,33 @@ const AllView = new Lang.Class({
         this._refilterApps();
     },
 
+    // Overriden from BaseAppView
+    animate: function (animationDirection, onComplete) {
+        if (animationDirection == IconGrid.AnimationDirection.OUT &&
+            this._displayingPopup && this._currentPopup) {
+            this._currentPopup.popdown();
+            let spaceClosedId = this._grid.connect('space-closed', Lang.bind(this,
+                function() {
+                    this._grid.disconnect(spaceClosedId);
+                    // Given that we can't call this.parent() inside the
+                    // signal handler, call again animate which will
+                    // call the parent given that popup is already
+                    // closed.
+                    this.animate(animationDirection, onComplete);
+                }));
+        } else {
+            this.parent(animationDirection, onComplete);
+        }
+    },
+
     getCurrentPageY: function() {
-        return this._grid.getPageY(this._currentPage);
+        return this._grid.getPageY(this._grid.currentPage);
     },
 
     goToPage: function(pageNumber) {
         pageNumber = clamp(pageNumber, 0, this._grid.nPages() - 1);
 
-        if (this._currentPage == pageNumber && this._displayingPopup && this._currentPopup)
+        if (this._grid.currentPage == pageNumber && this._displayingPopup && this._currentPopup)
             return;
         if (this._displayingPopup && this._currentPopup)
             this._currentPopup.popdown();
@@ -482,7 +535,7 @@ const AllView = new Lang.Class({
         let time;
         // Only take the velocity into account on page changes, otherwise
         // return smoothly to the current page using the default velocity
-        if (this._currentPage != pageNumber) {
+        if (this._grid.currentPage != pageNumber) {
             let minVelocity = totalHeight / (PAGE_SWITCH_TIME * 1000);
             velocity = Math.max(minVelocity, velocity);
             time = (diffToPage / velocity) / 1000;
@@ -493,9 +546,9 @@ const AllView = new Lang.Class({
         // longer than PAGE_SWITCH_TIME
         time = Math.min(time, PAGE_SWITCH_TIME);
 
-        this._currentPage = pageNumber;
+        this._grid.currentPage = pageNumber;
         Tweener.addTween(this._adjustment,
-                         { value: this._grid.getPageY(this._currentPage),
+                         { value: this._grid.getPageY(this._grid.currentPage),
                            time: time,
                            transition: 'easeOutQuad' });
         this._pageIndicators.setCurrentPage(pageNumber);
@@ -524,9 +577,9 @@ const AllView = new Lang.Class({
 
         let direction = event.get_scroll_direction();
         if (direction == Clutter.ScrollDirection.UP)
-            this.goToPage(this._currentPage - 1);
+            this.goToPage(this._grid.currentPage - 1);
         else if (direction == Clutter.ScrollDirection.DOWN)
-            this.goToPage(this._currentPage + 1);
+            this.goToPage(this._grid.currentPage + 1);
 
         return Clutter.EVENT_STOP;
     },
@@ -566,10 +619,10 @@ const AllView = new Lang.Class({
             return Clutter.EVENT_STOP;
 
         if (event.get_key_symbol() == Clutter.Page_Up) {
-            this.goToPage(this._currentPage - 1);
+            this.goToPage(this._grid.currentPage - 1);
             return Clutter.EVENT_STOP;
         } else if (event.get_key_symbol() == Clutter.Page_Down) {
-            this.goToPage(this._currentPage + 1);
+            this.goToPage(this._grid.currentPage + 1);
             return Clutter.EVENT_STOP;
         }
 
@@ -630,7 +683,7 @@ const AllView = new Lang.Class({
 
         if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != this._grid.nPages()) {
             this._adjustment.value = 0;
-            this._currentPage = 0;
+            this._grid.currentPage = 0;
             Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this,
                 function() {
                     this._pageIndicators.setNPages(this._grid.nPages());
@@ -792,6 +845,19 @@ const AppDisplay = new Lang.Class({
         let layout = new ControlsBoxLayout({ homogeneous: true });
         this._controls = new St.Widget({ style_class: 'app-view-controls',
                                          layout_manager: layout });
+        this._controls.connect('notify::mapped', Lang.bind(this,
+            function() {
+                // controls are faded either with their parent or
+                // explicitly in animate(); we can't know how they'll be
+                // shown next, so make sure to restore their opacity
+                // when they are hidden
+                if (this._controls.mapped)
+                  return;
+
+                Tweener.removeTweens(this._controls);
+                this._controls.opacity = 255;
+            }));
+
         layout.hookup_style(this._controls);
         this.actor.add_actor(new St.Bin({ child: this._controls }));
 
@@ -815,6 +881,29 @@ const AppDisplay = new Lang.Class({
         this._updateFrequentVisibility();
     },
 
+    animate: function(animationDirection, onComplete) {
+        let currentView = this._views[global.settings.get_uint('app-picker-view')].view;
+
+        // Animate controls opacity using iconGrid animation time, since
+        // it will be the time the AllView or FrequentView takes to show
+        // it entirely.
+        let finalOpacity;
+        if (animationDirection == IconGrid.AnimationDirection.IN) {
+            this._controls.opacity = 0;
+            finalOpacity = 255;
+        } else {
+            finalOpacity = 0
+        }
+
+        Tweener.addTween(this._controls,
+                         { time: IconGrid.ANIMATION_TIME_IN,
+                           transition: 'easeInOutQuad',
+                           opacity: finalOpacity,
+                          });
+
+        currentView.animate(animationDirection, onComplete);
+    },
+
     _showView: function(activeIndex) {
         for (let i = 0; i < this._views.length; i++) {
             let actor = this._views[i].view.actor;
@@ -964,6 +1053,7 @@ const FolderView = new Lang.Class({
         Util.ensureActorVisibleInScrollView(this.actor, actor);
     },
 
+    // Overriden from BaseAppView
     animate: function(animationDirection) {
         this._grid.animatePulse(animationDirection);
     },
index 5cd32c18437095fba5345f3c5cc926cbeaaf0e1f..31d4ea3098fcaa3ea85ceab6fc443f78855120b6 100644 (file)
@@ -18,12 +18,16 @@ const MIN_ICON_SIZE = 16;
 const EXTRA_SPACE_ANIMATION_TIME = 0.25;
 
 const ANIMATION_TIME_IN = 0.350;
+const ANIMATION_TIME_OUT = 1/2 * ANIMATION_TIME_IN;
 const ANIMATION_MAX_DELAY_FOR_ITEM = 2/3 * ANIMATION_TIME_IN;
+const ANIMATION_MAX_DELAY_OUT_FOR_ITEM = 2/3 * ANIMATION_TIME_OUT;
+const ANIMATION_FADE_IN_TIME_FOR_ITEM = 1/4 * ANIMATION_TIME_IN;
 
 const ANIMATION_BOUNCE_ICON_SCALE = 1.1;
 
 const AnimationDirection = {
-    IN: 0
+    IN: 0,
+    OUT: 1
 };
 
 const BaseIcon = new Lang.Class({
@@ -420,6 +424,118 @@ const IconGrid = new Lang.Class({
         }
     },
 
+    animateSpring: function(animationDirection, sourceActor) {
+        if (this._animating)
+            return;
+
+        this._animating = true;
+
+        let actors = this._getChildrenToAnimate();
+        if (actors.length == 0) {
+            this._animationDone();
+            return;
+        }
+
+        let [sourceX, sourceY] = sourceActor.get_transformed_position();
+        let [sourceWidth, sourceHeight] = sourceActor.get_size();
+        // Get the center
+        let [sourceCenterX, sourceCenterY] = [sourceX + sourceWidth / 2, sourceY + sourceHeight / 2];
+        // Design decision, 1/2 of the source actor size.
+        let [sourceScaledWidth, sourceScaledHeight] = [sourceWidth / 2, sourceHeight / 2];
+
+        actors.forEach(function(actor) {
+            let [actorX, actorY] = actor._transformedPosition = actor.get_transformed_position();
+            let [x, y] = [actorX - sourceX, actorY - sourceY];
+            actor._distance = Math.sqrt(x * x + y * y);
+        });
+        let maxDist = actors.reduce(function(prev, cur) {
+            return Math.max(prev, cur._distance);
+        }, 0);
+        let minDist = actors.reduce(function(prev, cur) {
+            return Math.min(prev, cur._distance);
+        }, Infinity);
+        let normalization = maxDist - minDist;
+
+        for (let index = 0; index < actors.length; index++) {
+            let actor = actors[index];
+            actor.opacity = 0;
+
+            let actorClone = new Clutter.Clone({ source: actor });
+            Main.uiGroup.add_actor(actorClone);
+
+            let [width, height,,] = this._getAllocatedChildSizeAndSpacing(actor);
+            actorClone.set_size(width, height);
+            let scaleX = sourceScaledWidth / width;
+            let scaleY = sourceScaledHeight / height;
+            let [adjustedSourcePositionX, adjustedSourcePositionY] = [sourceCenterX - sourceScaledWidth / 2, sourceY - sourceScaledHeight / 2];
+
+            // Defeat onComplete anonymous function closure
+            let isLastItem = index == actors.length - 1;
+
+            let movementParams, fadeParams;
+            if (animationDirection == AnimationDirection.IN) {
+                actorClone.opacity = 0;
+                actorClone.set_scale(scaleX, scaleY);
+
+                actorClone.set_position(adjustedSourcePositionX, adjustedSourcePositionY);
+
+                let delay = (1 - (actor._distance - minDist) / normalization) * ANIMATION_MAX_DELAY_FOR_ITEM;
+                let [finalX, finalY]  = actor._transformedPosition;
+                movementParams = { time: ANIMATION_TIME_IN,
+                                   transition: 'easeInOutQuad',
+                                   delay: delay,
+                                   x: finalX,
+                                   y: finalY,
+                                   scale_x: 1,
+                                   scale_y: 1,
+                                   onComplete: Lang.bind(this, function() {
+                                       if (isLastItem)
+                                           this._animationDone();
+
+                                       actor.opacity = 255;
+                                       actorClone.destroy();
+                                   })};
+                fadeParams = { time: ANIMATION_FADE_IN_TIME_FOR_ITEM,
+                               transition: 'easeInOutQuad',
+                               delay: delay,
+                               opacity: 255 };
+            } else {
+                let [startX, startY]  = actor._transformedPosition;
+                actorClone.set_position(startX, startY);
+
+                let delay = (actor._distance - minDist) / normalization * ANIMATION_MAX_DELAY_OUT_FOR_ITEM;
+                movementParams = { time: ANIMATION_TIME_OUT,
+                                   transition: 'easeInOutQuad',
+                                   delay: delay,
+                                   x: adjustedSourcePositionX,
+                                   y: adjustedSourcePositionY,
+                                   scale_x: scaleX,
+                                   scale_y: scaleY,
+                                   onComplete: Lang.bind(this, function() {
+                                       if (isLastItem) {
+                                           this._animationDone();
+                                           this._restoreItemsOpacity();
+                                       }
+                                       actorClone.destroy();
+                                   })};
+                fadeParams = { time: ANIMATION_FADE_IN_TIME_FOR_ITEM,
+                               transition: 'easeInOutQuad',
+                               delay: ANIMATION_TIME_OUT + delay - ANIMATION_FADE_IN_TIME_FOR_ITEM,
+                               opacity: 0 };
+            }
+
+
+            Tweener.addTween(actorClone, movementParams);
+            Tweener.addTween(actorClone, fadeParams);
+        }
+    },
+
+    _restoreItemsOpacity: function() {
+        for (let index = 0; index < this._items.length; index++) {
+            this._items[index].actor.opacity = 255;
+        }
+    },
+
     _getAllocatedChildSizeAndSpacing: function(child) {
         let [,, natWidth, natHeight] = child.get_preferred_size();
         let width = Math.min(this._getHItemSize(), natWidth);
@@ -632,6 +748,7 @@ const PaginatedIconGrid = new Lang.Class({
     _init: function(params) {
         this.parent(params);
         this._nPages = 0;
+        this.currentPage = 0;
         this._rowsPerPage = 0;
         this._spaceBetweenPages = 0;
         this._childrenPerPage = 0;
@@ -695,6 +812,15 @@ const PaginatedIconGrid = new Lang.Class({
         }
     },
 
+    // Overriden from IconGrid
+    _getChildrenToAnimate: function() {
+        let children = this._getVisibleChildren();
+        let firstIndex = this._childrenPerPage * this.currentPage;
+        let lastIndex = firstIndex + this._childrenPerPage;
+
+        return children.slice(firstIndex, lastIndex);
+    },
+
     _computePages: function (availWidthPerPage, availHeightPerPage) {
         let [nColumns, usedWidth] = this._computeLayout(availWidthPerPage);
         let nRows;
index 2affbb25642e9cfc5bac236bca08b9503d9713f3..84c070c60cef0c34f43f577def0fcea6ae038c58 100644 (file)
@@ -675,6 +675,10 @@ const Overview = new Lang.Class({
             this.hide();
         else
             this.show();
+    },
+
+    getShowAppsButton: function() {
+        return this._dash.showAppsButton;
     }
 });
 Signals.addSignalMethods(Overview.prototype);
index 9b1c6047b74adc96dfe831f703ce1b6eea296495..88ef32ce60503067be8085e8fda71248f8f8cd8a 100644 (file)
@@ -20,6 +20,7 @@ const ShellEntry = imports.ui.shellEntry;
 const Tweener = imports.ui.tweener;
 const WorkspacesView = imports.ui.workspacesView;
 const EdgeDragAction = imports.ui.edgeDragAction;
+const IconGrid = imports.ui.iconGrid;
 
 const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
 
@@ -301,18 +302,55 @@ const ViewSelector = new Lang.Class({
         return page;
     },
 
-    _fadePageIn: function(oldPage) {
+    _fadePageIn: function() {
+        Tweener.addTween(this._activePage,
+                         { opacity: 255,
+                           time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
+                           transition: 'easeOutQuad'
+                         });
+    },
+
+    _fadePageOut: function(page) {
+        let oldPage = page;
+        Tweener.addTween(page,
+                         { opacity: 0,
+                           time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: Lang.bind(this, function() {
+                               this._animateIn(oldPage);
+                           })
+                         });
+    },
+
+    _animateIn: function(oldPage) {
         if (oldPage)
             oldPage.hide();
 
         this.emit('page-empty');
 
         this._activePage.show();
-        Tweener.addTween(this._activePage,
-            { opacity: 255,
-              time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
-              transition: 'easeOutQuad'
-            });
+
+        if (this._activePage == this._appsPage && oldPage == this._workspacesPage) {
+            // Restore opacity, in case we animated via _fadePageOut
+            this._activePage.opacity = 255;
+            this.appDisplay.animate(IconGrid.AnimationDirection.IN);
+        } else {
+            this._fadePageIn();
+        }
+    },
+
+    _animateOut: function(page) {
+        let oldPage = page;
+        if (page == this._appsPage &&
+            this._activePage == this._workspacesPage &&
+            !Main.overview.animationInProgress) {
+            this.appDisplay.animate(IconGrid.AnimationDirection.OUT, Lang.bind(this,
+                function() {
+                    this._animateIn(oldPage)
+                }));
+        } else {
+            this._fadePageOut(page);
+        }
     },
 
     _showPage: function(page) {
@@ -327,17 +365,9 @@ const ViewSelector = new Lang.Class({
         this.emit('page-changed');
 
         if (oldPage)
-            Tweener.addTween(oldPage,
-                             { opacity: 0,
-                               time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
-                               transition: 'easeOutQuad',
-                               onComplete: Lang.bind(this,
-                                   function() {
-                                       this._fadePageIn(oldPage);
-                                   })
-                             });
+            this._animateOut(oldPage)
         else
-            this._fadePageIn();
+            this._animateIn();
     },
 
     _a11yFocusPage: function(page) {